package com.jeebey.mybatis.utils;

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMap;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.ObjectTypeHandler;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.jeebey.mybatis.annotation.GeneratedPosition;
import com.jeebey.mybatis.annotation.GeneratedStrategy;
import com.jeebey.mybatis.dialect.IDialect;
import com.jeebey.mybatis.query.Criterion;
import com.jeebey.mybatis.query.Parameter;

/**
 * @since 1.0
 * @author <a href="mailto:84961426@qq.com">JuST4iT</a>
 * @version $Id: MappedStatmentUtil.java Feb 18, 2016 6:06:57 PM $
 */

public class MappedStatmentUtil {

	protected static Log log = LogFactory.getLog(MappedStatmentUtil.class);

	public static Map<String, String> fields_cache = Maps.newHashMap();

	public static Map<String, Class<?>> clazzs_cache = Maps.newHashMap();

	public static final String FILED_PARTTERN="#\\[(.*?)\\]";
	/**
	 * 泛型-->类
	 * 
	 * @param resource
	 * @return
	 */
	public static Class<?> transResourceToEntityClass(String resource) {
		Class<?> clazz = clazzs_cache.get(resource);
		try {
			if (clazz == null) {
				String _clazzs = resource.replace(".java (best guess)", "").replace("/", ".");
				clazz = (Class<?>) ((ParameterizedType) Class.forName(_clazzs).getGenericInterfaces()[0])
						.getActualTypeArguments()[0];
				clazzs_cache.put(resource, clazz);
			}
		} catch (Exception e) {
			e.printStackTrace();
			log.error(e.getMessage());
		}
		return clazz;
	}

	public static MappedStatement createGenericMappedStatement(Class<?> clazz, MappedStatement mappedStatement,
			String sourceSql, Object parameterObject, List<Parameter> parameters,IDialect dialect) {
		SqlObject _sqlObj = createGenericSqlObject(mappedStatement, sourceSql, parameterObject, parameters, clazz);
		return createMappedStatement(clazz, _sqlObj, dialect);
	}
	
	public static MappedStatement createCriterionMappedStatement( MappedStatement mappedStatement,
			String sourceSql, Object parameterObject, List<Parameter> parameters,IDialect dialect) {
		SqlObject _sqlObj = createGenericSqlObject(mappedStatement, sourceSql, parameterObject, parameters, null);
		return createMappedStatement(null, _sqlObj, dialect);
	}

	public static SqlObject createGenericSqlObject(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<Parameter> parameters, Class<?> clazz) {
		Configuration configuration = mappedStatement.getConfiguration();
		List<ParameterMapping> parameterMappings = Lists.newArrayList();
		KeysObject keysObject = new KeysObject();

		for (Parameter parameter : parameters) {
			ParameterMapping.Builder builder = null;
			if (parameter.getValue() != null) {
				builder = new ParameterMapping.Builder(configuration, parameter.getField(), parameter.getValue()
						.getClass());
			} else {
				builder = new ParameterMapping.Builder(configuration, parameter.getField(), new ObjectTypeHandler());
			}
			parameterMappings.add(builder.build());
			keysObject.keyColumn(parameter.getKeyColumn());
			if(keysObject.keyProperty(parameter.getKeyProperty())){
				keysObject.javaType(parameter.getJavaType());
			}
			
			
			
			if(keysObject.isGenerated()==false){
				keysObject.setPrepose(!GeneratedPosition.AFTER.equals(parameter.getLocation()));
				keysObject.setStrategy(parameter.getStrategy());
				keysObject.setReferType(parameter.getReferType());
			}
			
			keysObject.setGenerator(parameter.getGenerator());
			keysObject.setGenerated(parameter.isGenerated()); // 变成真后 赋值无效
		}

		return new SqlObject(sourceSql, parameterObject, keysObject, mappedStatement, parameterMappings);
	}

	public static MappedStatement createPaginationMappedStatement(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings, RowBounds rowBounds, IDialect dialect) {
		String _sourceSql = dialect.createPagingSql(sourceSql, rowBounds.getOffset(), rowBounds.getLimit());
		SqlObject _sqlObj = createPaginationSqlObject(mappedStatement, _sourceSql, parameterObject, parameterMappings);
		return createMappedStatement(null, _sqlObj);
	}

	public static SqlObject createPaginationSqlObject(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings) {

		return new SqlObject(sourceSql, parameterObject, mappedStatement, parameterMappings);
	}
	
	public static MappedStatement createCriterionMappedStatement(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings,Criterion criterion) {
		sourceSql +=criterion.toSQL(); 
		SqlObject sqlObj = new SqlObject(sourceSql, parameterObject, mappedStatement, parameterMappings);
		return createMappedStatement(null, sqlObj);
	}

	public static MappedStatement createFiledsMappedStatement(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings) {

		String _clazz = null;
		String _field = null;
		Class<?> clazz = null;
		Pattern pattern = Pattern.compile(FILED_PARTTERN);
		Matcher matcher = pattern.matcher(sourceSql);
		if (matcher.find()) {
			_clazz = matcher.group(1);
		}

		String clazz_fileds = fields_cache.get(_clazz);
		if (clazz_fileds == null) {
			if(_clazz.contains("(")){
				_field=_clazz.substring(_clazz.indexOf("(")+1,_clazz.indexOf(")"));
				_clazz=_clazz.substring(0,_clazz.indexOf("("));
			}
			try {
				clazz = Class.forName(_clazz);
			} catch (Exception e) {
			}
			clazz_fileds = SqlGenerateUtil.createSqlSelectFileds(clazz,_field);
			fields_cache.put(_clazz, clazz_fileds);
		}
		sourceSql = StringUtils.replacePattern(sourceSql, "#\\[.*?\\]", clazz_fileds);
		SqlObject sqlObj = new SqlObject(sourceSql, parameterObject, mappedStatement, parameterMappings);
		return createMappedStatement(clazz, sqlObj);
	}

	public static MappedStatement createParameterMappedStatement(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings) {
		SqlObject sqlObj = createParameterSqlObject(mappedStatement, sourceSql, parameterObject, parameterMappings);
		return createMappedStatement(null, sqlObj);
	}

	
	@SuppressWarnings("unchecked")
	public static SqlObject createParameterSqlObject(MappedStatement mappedStatement, String sourceSql,
			Object parameterObject, List<ParameterMapping> parameterMappings) {
		Configuration configuration = mappedStatement.getConfiguration();
		TreeMap<Integer, TempObject> treeMap = Maps.newTreeMap();
		if (parameterObject instanceof Map) {
			Map<String, Object> parameterMap = (Map<String, Object>) parameterObject;
			for (Map.Entry<String, Object> m : parameterMap.entrySet()) {
				if (!m.getKey().startsWith("param")
						&& (m.getValue() instanceof Collection || m.getValue().getClass().isArray())) {
					Object[] objVals = m.getValue().getClass().isArray() ? (Object[]) m.getValue()
							: ((Collection<Object>) m.getValue()).toArray();
					Object[] locVals = new Object[objVals.length];
					Arrays.fill(locVals, "?");
					String keyArrays = isCollectionAndOnlyOne(parameterMap) ? "$[]" : "$[" + m.getKey() + "]";
					String sqlArrays = StringUtils.join("(", StringUtils.join(locVals, ","), ")");
					for (int ifrom = 0; (ifrom = sourceSql.indexOf(keyArrays, ifrom + 1)) != -1;) {
						treeMap.put(ifrom, new TempObject(m.getKey(), keyArrays, sqlArrays, objVals, ifrom,
								TempObject.PARAM_TYPE_ARRAYS));
					}
				}
			}

			//
			for (int ifrom = 0; (ifrom = sourceSql.indexOf("?", ifrom + 1)) != -1;) {
				treeMap.put(ifrom, new TempObject(TempObject.PARAM_TYPE_NORMAL));
			}

			List<ParameterMapping> _parameterMappings = Lists.newArrayList();
			TempObject[] tempObjs = treeMap.values().toArray(new TempObject[0]);
			for (int i = 0, iNull = 0; i < tempObjs.length; i++) {
				TempObject _tempObj = tempObjs[i];
				if (_tempObj.getType() == TempObject.PARAM_TYPE_NORMAL) {
					_parameterMappings.add(parameterMappings.get(iNull++));
				} else {
					Object[] objVals = (Object[]) _tempObj.getObj();
					for (int k = 0; k < objVals.length; k++) {
						_parameterMappings.add(null);
					}
				}
			}
			parameterMappings = _parameterMappings;

			int iOffset = 0;
			for (int i = 0; i < tempObjs.length; i++) {
				TempObject _tempObj = tempObjs[i];
				if (_tempObj.getType() == TempObject.PARAM_TYPE_ARRAYS) {
					Object[] objVals = (Object[]) _tempObj.getObj();
					String objKey = _tempObj.getKey();
					sourceSql = StringUtils.replace(sourceSql, _tempObj.getRep(), _tempObj.getSql());
					for (int j = 0; j < objVals.length; j++) {
						Object obj = objVals[j];
						String key = objKey + j;
						parameterMap.put(key, obj);
						ParameterMapping _parameterMapping = obj != null ? new ParameterMapping.Builder(configuration,
								key, obj.getClass()).build() : new ParameterMapping.Builder(configuration, key,
								new ObjectTypeHandler()).build();
						parameterMappings.remove(iOffset + j);
						parameterMappings.add(iOffset + j, _parameterMapping);
					}
					iOffset += objVals.length;
				} else {
					iOffset++;
				}
			}

		}

		return new SqlObject(sourceSql, parameterObject, mappedStatement, parameterMappings);

	}

	public static MappedStatement createSelectKeyMappedStatment(MappedStatement mappedStatement,
			List<String> properties, String selectKeySql, Class<?> clazz) {
		String keyId = mappedStatement.getId() + SelectKeyGenerator.SELECT_KEY_SUFFIX;
		Configuration configuration = mappedStatement.getConfiguration();
		SqlSource sqlSource = new RawSqlSource(configuration, selectKeySql, clazz);

		MappedStatement.Builder builder = new MappedStatement.Builder(configuration, keyId, sqlSource,
				SqlCommandType.SELECT);
		builder.resource(mappedStatement.getResource());
		builder.fetchSize(null);
		builder.statementType(StatementType.STATEMENT);
		builder.keyGenerator(new NoKeyGenerator());
		for (String property : properties) {
			builder.keyProperty(property);
		}

		builder.keyColumn(null);
		builder.databaseId(null);
		// for 3.4.2	
		//builder.lang(configuration.getDefaultScriptingLanuageInstance());
		//builder.resulSets(null);
		builder.lang(configuration.getLanguageRegistry().getDefaultDriver());
		builder.resultSets(null);
		
		builder.resultOrdered(false);
		builder.timeout(configuration.getDefaultStatementTimeout());

		List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
		ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(configuration, builder.id()
				+ "-Inline", clazz, parameterMappings);
		builder.parameterMap(inlineParameterMapBuilder.build());

		List<ResultMap> resultMaps = new ArrayList<ResultMap>();
		ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, builder.id() + "-Inline",
				clazz, new ArrayList<ResultMapping>());
		
		resultMaps.add(inlineResultMapBuilder.build());
		builder.resultMaps(resultMaps);
		builder.resultSetType(null);

		builder.flushCacheRequired(false);
		builder.useCache(false);
		builder.cache(null);

		return builder.build();

	}
	
	public static MappedStatement createMappedStatement(Class<?> clazz, SqlObject sqlObject) {
		return createMappedStatement(clazz,sqlObject,null);
	}

	public static MappedStatement createMappedStatement(Class<?> clazz, SqlObject sqlObject,IDialect dialect) {
		MappedStatement mappedStatement = sqlObject.getMappedStatement();
		Configuration configuration = mappedStatement.getConfiguration();
		List<ParameterMapping> parameterMappings = sqlObject.getParameterMappings();
		StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, sqlObject.getSql(), parameterMappings);
		
		MappedStatement.Builder builder = new MappedStatement.Builder(configuration, mappedStatement.getId(),
				staticSqlSource, mappedStatement.getSqlCommandType());
		builder.fetchSize(mappedStatement.getFetchSize());
		builder.flushCacheRequired(mappedStatement.isFlushCacheRequired());
		builder.resultOrdered(mappedStatement.isResultOrdered());
		builder.cache(mappedStatement.getCache());
		builder.useCache(mappedStatement.isUseCache());
		builder.lang(mappedStatement.getLang());
		builder.parameterMap(mappedStatement.getParameterMap());
		builder.resource(mappedStatement.getResource());

		
		List<ResultMap> resultMaps = mappedStatement.getResultMaps();
		if (clazz != null) {
			Class<?> typeClazz = resultMaps.get(0).getType();
			if (typeClazz.equals(Object.class) || typeClazz.equals(List.class)) {
				String mappedStatementId = mappedStatement.getId();
				String resultMapId = mappedStatementId.substring(0, mappedStatementId.lastIndexOf("."))
						+ ".GenericObject";
				ResultMap.Builder resultMapBuilder = new ResultMap.Builder(configuration, resultMapId, clazz,
						new ArrayList<ResultMapping>());
				resultMaps = Lists.newArrayList(resultMapBuilder.build());
				
			}
		}
		builder.resultMaps(resultMaps);
		builder.resultSetType(mappedStatement.getResultSetType());
//
//		if (mappedStatement.getResulSets() != null) {
//			for (String resulSets : mappedStatement.getResulSets()) {
//				builder.resulSets(resulSets);
//			}
//		}
// for 3.4.2		
		if (mappedStatement.getResultSets() != null) {
			for (String resultSets : mappedStatement.getResultSets()) {
				builder.resultSets(resultSets);
			}
		}
		
		builder.statementType(mappedStatement.getStatementType());
		builder.databaseId(mappedStatement.getDatabaseId());
		builder.timeout(mappedStatement.getTimeout());

		//
		if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {
			KeysObject keysObject=sqlObject.getKeysObject();
			if(keysObject.isGenerated()){
				for (String keyProperty : keysObject.keyProperties()) {
					builder.keyProperty(keyProperty);
				}
				
				for (String keyColumn : keysObject.keyColumns()) {
					builder.keyColumn(keyColumn);
				}
				
				if(dialect!=null&&StringUtils.isEmpty(keysObject.getGenerator())){
					keysObject.setGenerator(dialect.createIdentitySql());
				}
				
				//GENERATOR,UUID,JDBC,IDENTITY,TABLE,SEQUENCE,AUTO ;
				
				if(GeneratedStrategy.JDBC.equals(keysObject.getStrategy())){ //mysql 自增
					builder.keyGenerator(new Jdbc3KeyGenerator());
				}else if(GeneratedStrategy.UUID.equals(keysObject.getStrategy())){ // UUID
					//SqlGenerateUtil line 已经实现
				}else if(GeneratedStrategy.SHORTUUID.equals(keysObject.getStrategy())){ // SHORT-UUID
					//SqlGenerateUtil line 已经实现
				}else if(GeneratedStrategy.IDENTITY.equals(keysObject.getStrategy())){ //mysql 自增
					builder.keyGenerator(new Jdbc3KeyGenerator());
				}else if(GeneratedStrategy.TABLE.equals(keysObject.getStrategy())){ 
					log.error(" 暂示未实现: TABLE "+dialect.toString());
				}else if(GeneratedStrategy.SEQUENCE.equals(keysObject.getStrategy())){ 
					log.error(" 暂示未实现: SEQUENCE "+dialect.toString());
				}else if(GeneratedStrategy.GENERATOR.equals(keysObject.getStrategy())){
					String keyId=mappedStatement.getId() + SelectKeyGenerator.SELECT_KEY_SUFFIX;
					if(configuration.getMappedStatementNames().contains(keyId)==false){
						MappedStatement selectKeyMappedStatment=createSelectKeyMappedStatment(mappedStatement,keysObject.keyProperties(),keysObject.getGenerator(),keysObject.getReferType());
						configuration.addMappedStatement(selectKeyMappedStatment);
					}
					MappedStatement keyStatement=configuration.getMappedStatement(keyId,true);
					SelectKeyGenerator keyGenerator=new SelectKeyGenerator(keyStatement,keysObject.isPrepose());
					builder.keyGenerator(keyGenerator);
				}else{
					builder.keyGenerator(new Jdbc3KeyGenerator()); //mysql 自增
				}
			}
		}
		return builder.build();
	}

	private static boolean isCollectionAndOnlyOne(Map<String, Object> paramMap) {
		if (paramMap.size() != 1) {
			return false;
		}
		
		String key = paramMap.keySet().toArray(new String[0])[0];
		return Lists.newArrayList("array", "list", "set").contains(key);
	}

}

class SqlObject {
	private String sql;
	private KeysObject keysObject;
	private Object parameterObject;
	private MappedStatement mappedStatement;
	private List<ParameterMapping> parameterMappings;

	public SqlObject() {

	}

	public SqlObject(String sql, Object parameterObject, MappedStatement mappedStatement,
			List<ParameterMapping> parameterMappings) {
		this.sql = sql;
		this.parameterObject = parameterObject;
		this.mappedStatement = mappedStatement;
		this.parameterMappings = parameterMappings;
	}

	public SqlObject(String sql, Object parameterObject, KeysObject keysObject, MappedStatement mappedStatement,
			List<ParameterMapping> parameterMappings) {
		this.sql = sql;
		this.keysObject = keysObject;
		this.parameterObject = parameterObject;
		this.mappedStatement = mappedStatement;
		this.parameterMappings = parameterMappings;
	}

	public String getSql() {
		return sql;
	}

	public void setSql(String sql) {
		this.sql = sql;
	}

	public KeysObject getKeysObject() {
		return keysObject;
	}

	public void setKeysObject(KeysObject keysObject) {
		this.keysObject = keysObject;
	}

	public Object getParameterObject() {
		return parameterObject;
	}

	public void setParameterObject(Object parameterObject) {
		this.parameterObject = parameterObject;
	}

	public List<ParameterMapping> getParameterMappings() {
		return parameterMappings;
	}

	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
		this.parameterMappings = parameterMappings;
	}

	public MappedStatement getMappedStatement() {
		return mappedStatement;
	}

	public void setMappedStatement(MappedStatement mappedStatement) {
		this.mappedStatement = mappedStatement;
	}

}

class KeysObject {
	private List<String> keyProperties = Lists.newArrayList();
	private List<String> keyColumns = Lists.newArrayList();
	private List<Class<?>> javaTypes = Lists.newArrayList();
	private String generator;
	private boolean prepose;
	private boolean generated;
	private GeneratedStrategy strategy;
	private Class<?> referType;
	
	public GeneratedStrategy getStrategy() {
		return strategy;
	}

	public void setStrategy(GeneratedStrategy strategy) {
		this.strategy = strategy;
	}

	public boolean isGenerated() {
		return generated;
	}

	public void setGenerated(boolean generated) {
		if (this.generated == false) {
			this.generated = generated;
		}
	}

	public boolean isPrepose() {
		return prepose;
	}

	public void setPrepose(boolean prepose) {
		this.prepose = prepose;
	}

	public String getGenerator() {
		return generator;
	}

	public void setGenerator(String generator) {
		if (StringUtils.isEmpty(this.generator)) {
			this.generator = generator;
		}
	}

	public List<Class<?>> javaTypes(){
		return javaTypes;
	}
	

	public void javaType(Class<?> type){
		 javaTypes.add(type);
	}
	
	public List<String> keyProperties() {
		return keyProperties;
	}

	public List<String> keyColumns() {
		return keyColumns;
	}

	public boolean keyProperty(String keyProperty) {
		if (StringUtils.isNotEmpty(keyProperty)) {
			keyProperties.add(keyProperty);
			return true;
		}
		return false;
	}
	
	

	public void keyColumn(String keyColumn) {
		if (StringUtils.isNotEmpty(keyColumn)) {
			keyColumns.add(keyColumn);
		}
	}

	public Class<?> getReferType() {
		return referType;
	}

	public void setReferType(Class<?> referType) {
		this.referType = referType;
	}
	
}

class TempObject {

	public static int PARAM_TYPE_NORMAL = 1;
	public static int PARAM_TYPE_ARRAYS = 2;

	private int type;
	private int idx;
	private String key; // ids
	private String rep; // $[ids]
	private String sql; // (?,?,?)
	private Object obj;

	public TempObject(int type) {
		this.type = type;
	}

	public TempObject(String key, String rep, String sql, Object obj, int idx, int type) {
		this.key = key;
		this.rep = rep;
		this.sql = sql;
		this.obj = obj;
		this.idx = idx;
		this.type = type;
	}

	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}

	public String getRep() {
		return rep;
	}

	public void setRep(String rep) {
		this.rep = rep;
	}

	public Object getObj() {
		return obj;
	}

	public void setObj(Object obj) {
		this.obj = obj;
	}

	public int getType() {
		return type;
	}

	public void setType(int type) {
		this.type = type;
	}

	public String getSql() {
		return sql;
	}

	public void setSql(String sql) {
		this.sql = sql;
	}

	public int getIdx() {
		return idx;
	}

	public void setIdx(int idx) {
		this.idx = idx;
	}

	@Override
	public String toString() {
		return this.key + " " + this.type;
	}

}
